package cn.android666.net.util.download

import okhttp3.*
import timber.log.Timber
import java.io.*
import java.lang.Exception
import java.net.SocketTimeoutException

/** 范围下载器，用于下载文件的指定范围(用于多线程断点续传下载) */
class RangeDownloader(
    private val okHttpClient: OkHttpClient,
    /** 文件下载地址 */
    private val downloadUrl: String,
    /** 目标文件，指定文件保存在哪里，以什么名字保存 */
    private val destFile: File,
    /** 范围索引，指定这是文件的第几部分内容 */
    private val rangeIndex: Int,
    /** 范围大小，指定文件的每一部分是多大 */
    private val rangeSize: Long,
    /** 此变量保存最后一个Range的Size，如果是其它Range请传0 */
    private val endRangeSize: Long,
    /** 下载回调 */
    private var callback: RangeDownloadCallback?
): Callback {

    /** 用于保存文件下载进度的临时文件路径 */
    private val tempFile = File("${destFile.absoluteFile}.temp")
    /** 用于保存当前已下载范围的大小 */
    private var downloadedSize = 0L
    /** 用于保存下载的开始位置 */
    private var rangeStart = 0L
    /** 用于指示是否需要停止下载 */
    private var stopDownload = false

    /** 停止下载 */
    fun stopDownloadRange() {
        stopDownload = true
        callback = null
    }

    /** 开始下载文件的一部分 */
    fun startDownloadRange() {
        try {
            stopDownload = false
            RandomAccessFile(tempFile, "rw").use { tempRaf ->
                tempRaf.seek(rangeIndex * 8L)  // 跳到保存下载进度的位置，因为不同rangeIndex的保存进度位置不一样，所以需要跳转，使用long保存，long占8个字节
                downloadedSize = tempRaf.readLong()  // 读取已下载的大小
                if (isDownloadComplete()) {
                        Timber.d("此Range $rangeIndex 已经下载完成")
                        return
                }

                rangeStart = rangeIndex * rangeSize + downloadedSize  // 范围下载的起始位置
                val rangeEnd = rangeIndex * rangeSize + (if (endRangeSize == 0L) rangeSize else endRangeSize) - 1 // 范围下载的结束位置，起始位置是从0开始的，所以结束位置需要减1。
                Timber.d("Range：${rangeIndex} 下载：$rangeStart - $rangeEnd")

                // 构建下载请求，告诉服务器从哪里开始下载，到哪结束
                val request = Request.Builder()
                    .url(downloadUrl)
                        /*
                         * 注：Range是从0开始的，假如文件总大小为3字节，分3线程下载这3个字节，则
                         * 线程1 Range为: bytes=0-0
                         * 线程2 则Range为: bytes=1-1
                         * 线程3 则Range为: bytes=2-2
                         * 可以认为文件内容就是一个byte[]，所以读取byte[]时下标是从0开始的，最大的下标则为文件大小减1
                         * 可以看到，range中，start最小为0，end最大为文件大小减1。其实end可以超出文件大小,服务器并不报错
                         * 只是读服务器的流时读取不到这么多而已，但是start不能等于或大于（文件大小-1），因为这样将无法从
                         * 服务器读取到任何内容，服务器会返回响应码416，表示你读取的内容超出了文件可读范围
                         */
                    .header("Range", "bytes=$rangeStart-$rangeEnd")
                    .build()

                okHttpClient.newCall(request).enqueue(this)
            }
        } catch (e: Exception) {
            if (destFile.exists()) destFile.delete()
            if (tempFile.exists()) tempFile.delete()
            Timber.w(e, "下载失败，读临时文件时出现异常, 下载url = $downloadUrl")
            callback?.onFail("下载失败，读取临时文件时出现异常: ${e.message}")
        }
    }

    override fun onFailure(call: Call, e: IOException) {
        val failMsg = if (e is SocketTimeoutException || (e is InterruptedIOException && e.message == "timeout")) {
            "下载文件超时"
        } else {
            "下载文件时出现异常，${e.javaClass.simpleName}: ${e.message}"
        }
        Timber.w(e, "下载文件时出现异常(还没开始读流), 下载url = $downloadUrl")
        callback?.onFail(failMsg)
    }

    override fun onResponse(call: Call, response: Response) {
        if (!response.isSuccessful) { callback?.onFail("下载文件失败，响应码：${response.code}, Range = ${call.request().header("Range")}, rangeIndex = $rangeIndex, rangeSize = $rangeSize"); return }
        val fileSize = response.body.contentLength()
        if (fileSize <= 0) { callback?.onFail("下载文件失败，文件大小为：$fileSize"); return }

        try {
            response.body.byteStream().use { inputStream ->
                RandomAccessFile(destFile, "rw").use { dataFile ->
                    RandomAccessFile(tempFile, "rw").use { tempFile ->
                        inputStreamToFile(inputStream, dataFile, tempFile)
                    }
                }
            }
        } catch (e: Exception) {
            Timber.w(e, "下载Range(${rangeIndex})时读流出现异常, 下载url = $downloadUrl")
            callback?.onFail("网络不稳定，下载文件失败(${e.message})")
        }
    }

    /** 把inputStream中的内容写到dataFile中，tempFile用于保存下载进度 */
    private fun inputStreamToFile(inputStream: InputStream, dataFile: RandomAccessFile, tempFile: RandomAccessFile) {
        dataFile.seek(rangeStart)
        val buffer = ByteArray(8192)
        var readSize: Int
        while (inputStream.read(buffer).also { readSize = it } != -1 && !stopDownload) {
            dataFile.write(buffer, 0, readSize) // 把数据写到dataFile
            downloadedSize += readSize               // 统计总共下载大小
            tempFile.seek(rangeIndex * 8L)      // 跳转到保存下载进度的位置
            tempFile.writeLong(downloadedSize)       // 保存下载进度
            // 注意：下面一行代码不能放在前面，因为在下载完成的时候会把tempFile删除掉，会导致前面的tempFile操作出现异常：IOException: write failed: ENOENT (No such file or directory)
            callback?.onLoading(readSize.toLong())   // 把下载的大小通知给回调，注：这里没有使用downloadedSize，是因为回调那边已经有累计了
        }
        if (isDownloadComplete()) {
            Timber.d("Range：$rangeIndex 下载完成，下载大小：$downloadedSize")
        } else {
            Timber.d("Range：$rangeIndex 下载未完成，已退出下载，rangeSize = ${if (endRangeSize == 0L) rangeSize else endRangeSize}, downloadSize = $downloadedSize")
        }
    }

    /** 判断是否下载完成了 */
    private fun isDownloadComplete() = if (endRangeSize > 0) (downloadedSize == endRangeSize) else (downloadedSize == rangeSize)
}